package i3.ui.option;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.GroupLayout;
import javax.swing.GroupLayout.Alignment;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.LayoutStyle.ComponentPlacement;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
/**
 * A multi value object option pane.
 * Appropriate to present Object options that only one of many values (known
 * before hand) can be selected.
 *
 * You insert the name of the option, the default value,
 * a array of possible values, a array of possible names for each value
 * (optional) and a general description.
 * You can get a current named value by calling get(String name)
 * with the name you placed. Since the different names can refer to different
 * classes, you have to do the cast yourself, when you get it. (Since in java
 * there is no way to bind the function return to a generic class on runtime.
 * (remember there is more than one return type possible, one for each option,
 * so generic class return at compile time is no solution.)
 *
 * If you want to listen add a propertyChangeListener to this, with the String
 * argument as the name of the option you put in addObjectOption. You can only
 * listen to the new value, the old value is null (limitation in this version)
 *
 * @author  i30817
 */
public final class ObjectOptions extends javax.swing.JPanel implements Iterable<Entry<String, Object>> {

    private boolean isAdjusting;

    /**
     * Creates new form ObjectOptions
     */
    public ObjectOptions() {
        super();
        initComponents();
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jScrollPane1 = new JScrollPane();
        optionsList = new JList<Object>(new DefaultListModel<Object>());
        jScrollPane2 = new JScrollPane();
        configurationList = new JList<String>(new DefaultListModel<String>());
        textPane = new JTextArea();

        jScrollPane1.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        jScrollPane1.setPreferredSize(new Dimension(262, 134));

        optionsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        optionsList.addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent evt) {
                optionsListValueChanged(evt);
            }
        });
        jScrollPane1.setViewportView(optionsList);

        jScrollPane2.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        configurationList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        configurationList.addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent evt) {
                configurationListValueChanged(evt);
            }
        });
        jScrollPane2.setViewportView(configurationList);

        textPane.setEditable(false);
        textPane.setColumns(20);
        textPane.setLineWrap(true);
        textPane.setRows(5);
        textPane.setWrapStyleWord(true);
        textPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        textPane.setFocusable(false);
        textPane.setMargin(new Insets(3, 3, 3, 3));
        textPane.setMinimumSize(new Dimension(0, 0));
        textPane.setOpaque(false);

        GroupLayout layout = new GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(ComponentPlacement.RELATED)
                .addComponent(jScrollPane1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(ComponentPlacement.RELATED)
                .addComponent(textPane, GroupLayout.DEFAULT_SIZE, 0, Short.MAX_VALUE)
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(Alignment.LEADING)
            .addGroup(Alignment.TRAILING, layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(Alignment.TRAILING)
                    .addComponent(textPane, Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 178, Short.MAX_VALUE)
                    .addComponent(jScrollPane1, Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 178, Short.MAX_VALUE)
                    .addComponent(jScrollPane2, Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 178, Short.MAX_VALUE))
                .addContainerGap())
        );
    }// </editor-fold>//GEN-END:initComponents

    private void optionsListValueChanged(javax.swing.event.ListSelectionEvent evt) {//GEN-FIRST:event_optionsListValueChanged
        if (evt.getValueIsAdjusting()) {
            return;
        }

        Object key = configurationList.getSelectedValue();

        Dual selectedDual = (Dual) optionsList.getSelectedValue();

        if (selectedDual != null && !isAdjusting) {
            objectMap.put(key.toString(), selectedDual);
            firePropertyChange(key.toString(), null, selectedDual.obj);
        }
    }//GEN-LAST:event_optionsListValueChanged

    private void configurationListValueChanged(javax.swing.event.ListSelectionEvent evt) {//GEN-FIRST:event_configurationListValueChanged
        if (evt.getValueIsAdjusting()) {
            return;
        }


        String key = configurationList.getSelectedValue();
        Collection values = valuesMap.get(key);
        Object current = objectMap.get(key);

        textPane.setText(descriptionMap.get(key));

        DefaultListModel<Object> mod = (DefaultListModel<Object>) optionsList.getModel();
        mod.clear();

        for (Object value : values) {
            mod.addElement(value);
        }

        isAdjusting = true;
        optionsList.setSelectedValue(current, true);
        isAdjusting = false;
    }//GEN-LAST:event_configurationListValueChanged

    /**
     * Adds a Object option. Always sends the current value and a old value of null
     * to registered listeners (it is supposted to be current, so it won't change if it really is,
     * and saves some code if it isn't).
     * @param name not null
     *@param current not null and one of the possibleValues. If its not, the options list won't be selected
     *at the start (not very serious).
     *@param possibleValues not null, and not empty, with all the values that have the same class as the
     *current argument.
     *@param valueNames can be null. If not null must be same size as possibleValues and
     *is used instead of toString of each possibleValue to name each object in the user interface. Strings
     *inside valueNames can be null, and in that case the string presented is the toString of the equivalent object.
     *@param objectsDescription a general description of the setting, again can be null.
     *@throws IllegalArgumentException if the name, the current object the possiblevalues array is null
     */
    public void addObjectOption(String name, Object current, Object[] possibleValues, String[] valueNames, String objectsDescription) {
        if (name == null || current == null || possibleValues == null) {
            throw new IllegalArgumentException("Name, or current object, or possible values is null");
        }

        if (valueNames != null && valueNames.length != possibleValues.length) {
            throw new IllegalArgumentException("valueNames is not null, but its length is different from the length of possibleValues");
        }

        if (!objectMap.containsKey(name)) {
            DefaultListModel<String> mod = (DefaultListModel<String>) configurationList.getModel();
            mod.addElement(name);
        }

        Dual currentDual = new Dual(current, null);
        objectMap.put(name, currentDual);
        Collection<Dual> possibleValuesDuals = new ArrayList<>(possibleValues.length);

        if (valueNames == null) {
            for (Object possibleValue : possibleValues) {
                currentDual = new Dual(possibleValue, null);
                possibleValuesDuals.add(currentDual);
                if (currentDual.obj.equals(current)) {
                    objectMap.put(name, currentDual);
                }
            }
        } else {
            for (int i = 0; i < possibleValues.length; i++) {
                currentDual = new Dual(possibleValues[i], valueNames[i]);
                possibleValuesDuals.add(currentDual);
                if (currentDual.obj.equals(current)) {
                    objectMap.put(name, currentDual);
                }
            }
        }

        valuesMap.put(name, possibleValuesDuals);
        descriptionMap.put(name, objectsDescription);
        //send the first event (updates any listener to the first value)
        firePropertyChange(name, null, current);
        if (configurationList.getSelectedIndex() == -1) {
            configurationList.setSelectedIndex(0);
        }
    }

    /**
     * Adds a Object option using the default description (from the toString of each option).
     * Always sends the current value and a old value of null
     * to registered listeners (it is supposted to be current, so it won't change if it really is,
     * and saves some code if it isn't).
     * @param name not null
     *@param current not null
     *@param possibleValues not null, and not empty, with all the values that have the same class, or superclass
     * as the current argument.
     * @param objectDescription a general description of the setting.
     * @throws IllegalArgumentException if the name, the current object the possiblevalues array is null
     */
    public void addObjectOption(String name, Object current, Object[] possibleValues, String objectDescription) {
        addObjectOption(name, current, possibleValues, null, objectDescription);
    }

    public Object get(String name) {
        Dual d = objectMap.get(name);
        if (d == null) {
            return null;
        } else {
            return d.obj;
        }
    }

    public void put(String name, Object o) {
        Dual d = objectMap.get(name);
        if (d != null) {
            d.obj = o;
        }
    }

    @Override
    public Iterator<Entry<String, Object>> iterator() {
        Set<Entry<String, Object>> entries = new HashSet<>(objectMap.size());
        for (Entry<String, Dual> e : objectMap.entrySet()) {
            entries.add(new SimpleEntry<>(e.getKey(), e.getValue().obj));
        }
        return entries.iterator();
    }

    public void updateSize() {
        int width = 1;
        ListModel<String> m = configurationList.getModel();
        ListCellRenderer<? super String> lcr = configurationList.getCellRenderer();
        for (int i = 0; i < m.getSize(); i++) {
            String o = m.getElementAt(i);
            Component c = lcr.getListCellRendererComponent(configurationList, o, 0, false, false);
            Dimension dim = c.getPreferredSize();
            width = Math.max(dim.width, width);
        }
        width = (int) (width + jScrollPane2.getVerticalScrollBar().getPreferredSize().getWidth());
        jScrollPane2.setPreferredSize(new Dimension(width, -1));

        width = 1;
        ListCellRenderer<? super Object> olcr = optionsList.getCellRenderer();
        for (Collection options : valuesMap.values()) {
            for (Object o : options) {
                Component c = olcr.getListCellRendererComponent(optionsList, o, 0, false, false);
                Dimension dim = c.getPreferredSize();
                width = Math.max(dim.width, width);
            }
        }

        width = (int) (width + jScrollPane1.getVerticalScrollBar().getPreferredSize().getWidth());
        jScrollPane1.setPreferredSize(new Dimension(width, -1));
    }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private JList<String> configurationList;
    private JScrollPane jScrollPane1;
    private JScrollPane jScrollPane2;
    private JList<Object> optionsList;
    private JTextArea textPane;
    // End of variables declaration//GEN-END:variables
    private Map<String, Dual> objectMap = new HashMap<>();
    private Map<String, Collection<Dual>> valuesMap = new HashMap<>();
    private Map<String, String> descriptionMap = new HashMap<>();

    private final static class Dual {

        private String desc;
        private Object obj;

        public Dual(Object o, String s) {
            obj = o;
            desc = s;
        }

        @Override
        public String toString() {
            if (desc == null) {
                return obj.toString();
            } else {
                return desc;
            }
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Dual other = (Dual) obj;
            if (this.obj != other.obj && (this.obj == null || !this.obj.equals(other.obj))) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            return obj.hashCode();
        }
    }
}
